/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package projectshanghai;

import Actor.*;
import Map.MapLoader;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import javax.vecmath.Point2d;

/**
 *
 * @author Eric-Standard
 */
public class World {

    private Map.Map map;
    private Dimension mapSize;
    private int width;
    private int height;
    private Dimension size, mapViewSize;
    //ArrayList and a buffer to add to the ArrayList
    private ArrayList<Actor> actors = new ArrayList<Actor>();
    private ArrayList<Actor> actorsBuffer = new ArrayList<Actor>();
    private ArrayList<Projectile> projectiles = new ArrayList<Projectile>();
    private ArrayList<Projectile> projectilesBuffer = new ArrayList<Projectile>();
    //ArrayList for hit markers
    private ArrayList<BulletHitMarker> bulletHitMarkers = new ArrayList<BulletHitMarker>();
    public final Camera cam;
    private Player player;
    //Height of the HUD at the top and bottom of screen
    public static final int HUD_HEIGHT = 50;
    //Width of the HUD at each side of the screen
    public static final int HUD_WIDTH = 50;
    //Max dt every step
    public static final int MAX_DT = 500;
    public final Random rand;
    private Rectangle viewRect;
    public final KeyMonitor keyMonitor = new KeyMonitor();
    public final MouseMonitor mouseMonitor = new MouseMonitor();

    public World(int width, int height) {
        this.width = width;
        this.height = height;
        cam = new Camera(this);
        size = new Dimension(width, height);
        mapViewSize = new Dimension(Map.Map.MAP_WIDTH * Map.Map.TILE_WIDTH,
                Map.Map.MAP_WIDTH * Map.Map.TILE_WIDTH);
        rand = new Random();
    }

    /**
     * Loads a map.
     *
     * @param file
     */
    public void loadMap(File file) {
        map = MapLoader.loadMap(file);
        mapSize = new Dimension(Map.Map.TILE_WIDTH * Map.Map.MAP_WIDTH,
                Map.Map.TILE_HEIGHT * map.arr.length);
    }

    /**
     * Adds an actor to this world.
     *
     * @param a
     */
    public void add(Actor a) {
        a.putSelfInWorld(this);
        actorsBuffer.add(a);
    }

    /**
     * Adds a projectile to this world.
     *
     * @param p
     */
    public void add(Projectile p) {
        p.putSelfInWorld(this);
        projectilesBuffer.add(p);
    }

    /**
     * Sets the player.
     *
     * @param p
     */
    public void setPlayer(Player p) {
        player = p;
    }

    public List<Actor> getActors() {
        return actors;
    }

    /**
     * Gets the current player.
     *
     * @return
     */
    public Player getPlayer() {
        return player;
    }

    /**
     * Removes an actor.
     *
     * @param a The actor.
     */
    public void remove(Actor a) {
        a.removeSelfFromWorld();
    }

    /**
     * Updates this world.
     *
     * @param dt The time passed from the previous step.
     */
    public void step(long dt) {
        //Prevent high dt updates and skipping
        if (dt > MAX_DT) {
            dt = GamePanel.FRAME_RATE;
        }
        //Projectiles
        for (int i = 0; i < projectiles.size(); i++) {
            Projectile p = projectiles.get(i);
            if (p.isActive) {
                p.act(dt);
            }
            //Check for collisions with actors
            if (p.collisionDetection) {
                Shape shape = p.getShape();
                int team = 0;
                if (p instanceof TeamActor) {
                    TeamActor t = (TeamActor) p;
                    team = t.getTeam();
                }
                for (Actor a : actors) {
                    if (!p.collisionDetection) {
                        break;
                    } else if (!a.collisionDetection) {
                        continue;
                    } else if (p.owner == a) {
                        continue;
                    } else if (a instanceof TeamActor) {
                        TeamActor t = (TeamActor) a;
                        if (t.getTeam() != 0 && t.getTeam() == team) {
                            continue;
                        }
                    }
                    Rectangle2D boundRect = a.getBoundingRectangle();
                    if (shape.intersects(boundRect)) {
                        a.collide(p);
                        p.collide(a);
                    }
                }
            }
            removeInactiveActors();
            removeInactiveProjectiles();
        }
        //Call each actor's act() method and check for collisions
        for (Actor a : actors) {
            a.act(dt);
            Rectangle2D boundRect = a.getBoundingRectangle();
            //Check for collision with objects on the current tile
            Point tilePoint = getTilePointAt(a.getPos());
            if (map.isValid(tilePoint.x, tilePoint.y)) {
                Shape[] shapes =
                        map.arr[tilePoint.y][tilePoint.x].getShapes(
                        tilePoint.x * Map.Map.TILE_HEIGHT - (int) cam.x,
                        tilePoint.y * Map.Map.TILE_WIDTH - (int) cam.y);
                if (shapes != null) {
                    for (Shape s : shapes) {
                        if (s.intersects(boundRect)) {
                            a.collide(s);
                        }
                    }
                }
            }
        }
        checkCollisions();
        removeInactiveActors();
        addBufferedActors();
        addBufferedProjectiles();
        for (BulletHitMarker b : bulletHitMarkers) {
            b.act(dt);
        }
        Iterator<BulletHitMarker> hitsIter = bulletHitMarkers.iterator();
        while (hitsIter.hasNext()) {
            Actor actor = hitsIter.next();
            if (!actor.isActive) {
                hitsIter.remove();
            }
        }
    }

    /**
     * Adds buffered actors.
     */
    private void addBufferedActors() {
        actors.addAll(actorsBuffer);
        actorsBuffer.clear();
    }

    /**
     * Adds buffered projectiles.
     */
    private void addBufferedProjectiles() {
        projectiles.addAll(projectilesBuffer);
        projectilesBuffer.clear();
    }

    /**
     * Removes inactive actors from the actors list.
     */
    private void removeInactiveActors() {
        Iterator<Actor> actorIter = actors.iterator();
        while (actorIter.hasNext()) {
            Actor actor = actorIter.next();
            if (!actor.isActive) {
                actorIter.remove();
            }
        }
    }

    private boolean isInViewport(Actor a) {
        Rectangle2D r = a.getBoundingRectangle();
        if (r == null) {
            return false;
        }
        return r.intersects(viewRect);
    }

    private void checkCollisions() {
        Iterator<Actor> actorIter = actors.iterator();
        while (actorIter.hasNext()) {
            Actor actor = actorIter.next();
            Rectangle2D r = actor.getBoundingRectangle();
            Area area = actor.getArea();
            int team = 0;
            if (actor instanceof TeamActor) {
                TeamActor t = (TeamActor) actor;
                team = t.getTeam();
            }
            if (actor.collisionDetection) {
                //Check for collisions with other actors
                for (Actor a : actors) {
                    int teamA = 0;
                    if (a instanceof TeamActor) {
                        TeamActor t = (TeamActor) a;
                        teamA = t.getTeam();
                    }
                    //Check team
                    if (team == teamA && team != 0) {
                        continue;
                    }
                    if (a.collisionDetection) {
                        Rectangle2D r2 = a.getBoundingRectangle();
                        if (r.intersects(r2)) {
                            Area a1 = new Area(area);
                            a1.intersect(a.getArea());
                            if (!a1.isEmpty()) {
                                actor.collide(a);
                                a.collide(actor);
                            }
                        }
                    }
                }
                //Check for collisions with tiles
                Point tilePoint = getTilePointAt(actor.getPos());
                TileObject tileObject = map.getTileObject(tilePoint);
                if (tileObject != null) {
                    Rectangle2D r2 = tileObject.getBoundingRectangle();
                    if (r.intersects(r2)) {
                        Area a1 = new Area(area);
                        a1.intersect(tileObject.getArea());
                        if (!a1.isEmpty()) {
                            actor.collide(tileObject);
                            tileObject.collide(actor);
                        }
                    }
                }
            }
        }
    }

    /**
     * Removes inactive projectiles from the projectiles list.
     */
    private void removeInactiveProjectiles() {
        Iterator<Projectile> projIter = projectiles.iterator();
        while (projIter.hasNext()) {
            Projectile actor = projIter.next();
            if (!actor.isActive) {
                projIter.remove();
            }
        }
    }

    /**
     * Steps the world at the default framerate.
     */
    public void step() {
        step(GamePanel.FRAME_RATE);
    }

    /**
     * Tests if a point is valid in the world.
     *
     * @param x
     * @param y
     * @return
     */
    public boolean isValid(int x, int y) {
        if (x < 0 || y < 0) {
            return false;
        } else if (x >= map.arr.length * Map.Map.TILE_WIDTH || y >= map.arr[0].length * Map.Map.TILE_HEIGHT) {
            return false;
        }
        return true;
    }

    /**
     * Gets the map.
     *
     * @return
     */
    public Map.Map getMap() {
        return map;
    }

    /**
     * Gets the map size.
     *
     * @return
     */
    public Dimension getMapSize() {
        return mapSize;
    }

    /**
     * Gets the tile at a certain point. The return is another point
     * representing the coordinates of the tile.
     *
     * @param p
     * @return
     */
    public Point getTilePointAt(Point2d p) {
        Point result = new Point();
        result.x = (int) (p.x / Map.Map.TILE_WIDTH);
        result.y = (int) (p.y / Map.Map.TILE_HEIGHT);
        return result;
    }

    /**
     * Gets the size of this world.
     *
     * @return
     */
    public Dimension getSize() {
        return size;
    }

    /**
     * Draws this world on the Graphics context.
     *
     * @param gr
     */
    public void draw(Graphics gr) {
        final Graphics g = (Graphics) gr.create();
        try {
            //Draw the world
            drawWorld(g);
            //Draw HUD
            drawHUD(g);
            //Draw bullet hit arcs
            drawHitArcs(g);
        } finally {
            g.dispose();
        }
    }

    /**
     * Draws the hit markers on the Graphics context.
     *
     * @param gr
     */
    private void drawHitArcs(Graphics gr) {
        final Graphics g = (Graphics) gr.create();
        g.setColor(Color.RED);
        for (BulletHitMarker b : bulletHitMarkers) {
            b.draw(g);
        }
    }

    /**
     * Draws the world on the Graphics context.
     *
     * @param gr
     */
    private void drawWorld(Graphics gr) {
        final Graphics g = (Graphics) gr.create();
        //Move by HUD
        g.translate(HUD_WIDTH, HUD_HEIGHT);
        cam.moveToBounds();
        g.setClip(0, 0, mapViewSize.width, mapViewSize.height);
        g.translate((int) cam.x, -(int) cam.y);
        //Draw tiles
        Point topLeftTilePoint = getTilePointAt(cam);
        //There can be at most PANEL_HEIGHT/TILE_HEIGHT + 1 tiles showing
        for (int i = 0; i < height / Map.Map.TILE_HEIGHT + 1; i++) {
            int y = topLeftTilePoint.y + i;
            if (y < 0 || y >= map.arr.length) {
                continue;
            }
            for (int x = 0; x < map.arr[y].length; x++) {
                map.drawTile(g, x, y, x * Map.Map.TILE_HEIGHT, y * Map.Map.TILE_WIDTH);
            }
        }
        //Draw actors
        for (Actor a : actors) {
            if (a.isDrawn) {
                a.draw(g);
            }
        }
        //Draw projectiles
        for (int i = 0; i < projectiles.size(); i++) {
            projectiles.get(i).draw(g);
        }
        g.dispose();
    }

    /**
     * Draws the HUD on the Graphics context.
     */
    private void drawHUD(Graphics g) {
        player.drawHUD(g);
    }

    /**
     * Gets a List of Projectiles in this world.
     *
     * @return
     */
    public ArrayList<Projectile> getProjectiles() {
        return projectiles;
    }

    public Point2d getPointFromMousePoint(Point point) {
        Point2d p = new Point2d();
        p.y = point.y + cam.y - World.HUD_HEIGHT;
        p.x = point.x + cam.x - World.HUD_WIDTH;
        return p;
    }
}